package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.hmdp.utils.RedisConstants.*;

@Component
@Slf4j
public class CacheUtils {
    @Resource
    StringRedisTemplate stringRedisTemplate;

    private final static ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);


    /**
     * 将任意Java对象序列化为json并存储在string类型的key中，并且可以设置TTL过期时间
     */
    public void set(String key, Object value, Long time, TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,unit);
    }

    /**
     * 将任意Java对象序列化为json并存储在string类型的key中，并且可以设置逻辑过期时间，用于处理缓存击穿问题
     */
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 根据指定的key查询缓存，并反序列化为指定类型，利用缓存空值的方式解决缓存穿透问题
     */
    public <T,ID> T queryWithPassThrough(String key, ID id, Class<T> type, Function<ID,T> function, Long time,
                                                TimeUnit unit){
        //先查缓存
        String json = stringRedisTemplate.opsForValue().get(key+id);
        if (!StringUtils.isEmpty(json)) {
            return JSONUtil.toBean(json, type);
        }
        //情况1：缓存查不到，则是null,这种就是没有缓存，去查数据库
        //情况2：缓存查到了，但是值为"",这种就是数据库暂时没有数据，直接返回提示，这个缓存会设置一个短的有效期。
        if (json != null) {
            //预防缓存穿透，如果里面是""，则是有一个查询个查了一次数据库，但是数据库里没有数据
            //数据库没有数据，防止用户再次查询，又走数据库增大数据库压力。所以让缓存命中
            return null;
        }
        //缓存没有再查数据库
        T t = function.apply(id);
        if (ObjectUtil.isNull(t)) {
            //缓存一个空值，预防缓存穿透。
            stringRedisTemplate.opsForValue().set(key + id, "", time, unit);
            return null;
        }
        //写回缓存.设置超时时间，防止缓存更新失败，作为兜底。
        this.set(key+id,t,time,unit);
        return t;
    }
    /**
     * 根据指定的key查询缓存，并反序列化为指定类型，需要利用逻辑过期解决缓存击穿问题
     */
    public <T,ID> T queryWhitExpireLock(String key,ID id,Class<T> type,Function<ID,T> function, Long time,TimeUnit unit) {
        //1.先查缓存
        String redisResult = stringRedisTemplate.opsForValue().get(key + id);
        if (StringUtils.isEmpty(redisResult)) {
            return null;//一般后台系统会先将热门数据先加到缓存中，然后设置逻辑过期(实质是永不过期的，所以一定能查到数据，查不到要么redis挂了，要么不存在这个商铺)。这边用单元测试先预热一下数据
        }
        //1.2拿到数据先看看没有有过期，没过期直接返回，过期了重建
        RedisData redisData = JSONUtil.toBean(redisResult, RedisData.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        T t = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        if (expireTime.isAfter(LocalDateTime.now())) {
            log.info("没过期直接返回");
            return t;
        }
        //2.过期了重建数据
        //2.1获取锁
        boolean isLock = tryLock(key);
        //2.2拿到锁了，二次检查缓存中有没有数据，是否过期。没有再新建线程去重建缓存
        if (isLock) {
            String redisResultTemp = stringRedisTemplate.opsForValue().get(key + id);
            if (!StringUtils.isEmpty(redisResultTemp)) {
                RedisData redisDataTemp = JSONUtil.toBean(redisResult, RedisData.class);
                LocalDateTime expireTemp = redisDataTemp.getExpireTime();
                if (!expireTemp.isAfter(LocalDateTime.now())) {
                    //二次确认过期了，开启新线程去重建数据
                    CACHE_REBUILD_EXECUTOR.submit(() -> {
                        try {
                            log.info("重建数据");
                            T mysqlData = function.apply(id);
                            this.setWithLogicalExpire(key+id,mysqlData,time,unit);
                        } catch (Exception e) {
                            throw new RuntimeException("重建数据异常");
                        } finally {
                            //3.释放锁
                            unLock(key);
                            log.info("重建完成");
                        }
                    });
                }
            }

        }
        //4.没拿到锁先返回过期的数据
        log.info("没拿到锁返回旧数据");
        return t;
    }
    boolean tryLock(String key) {
        //防止线程异常锁没有被释放，给一个过期时间
        Boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(isLock);//直接返回包装类拆箱时可能出现NPE
    }

    void unLock(String key) {
        Boolean isLock = stringRedisTemplate.delete(key);
    }

}
